<?php

namespace fakis\core\base;

use fakis\core\traits\ModelAttributes;
use fakis\core\traits\ModelTrait;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use yii\db\ActiveQuery;
use yii\db\BaseActiveRecord;
use yii\db\QueryInterface;
use yii\helpers\ArrayHelper;

/**
 * 通用搜索模型
 *
 * @property array $relations
 * @property array $filters
 * @property-read ActiveDataProvider $dataProvider
 *
 * @author Fakis <fakis738@qq.com>
 */
class SearchModel extends Model
{
    use ModelTrait, ModelAttributes;

    const SCENARIO_SEARCH = 'ar_search';

    const FILTER_DEFAULT = 'filter_default';
    const FILTER_RANGE = 'filter_range';
    const FILTER_MATCH = 'filter_match';

    /**
     * 活动记录
     * @var string
     */
    public $arClass;

    /**
     * 数据绑定器参数
     * @var array
     */
    public $providerOptions = [];

    /**
     * 筛选范围符号
     * @var string
     */
    public $filterRangeParam = '~';

    /**
     * 验证失败时返回空记录
     * @var bool
     */
    public $returnEmptyWhenValidateFails = false;

    /**
     * 关联搜索表
     * @var array
     */
    private $_relations = [];

    /**
     * 自动筛选的属性
     * @var array
     */
    private $_filters = [];

    /**
     * 允许自动筛选的属性
     * @var array
     */
    private $_allowFilterAttributes = [];

    /**
     * @inheritDoc
     */
    public function init()
    {
        parent::init();

        if (!(is_string($this->arClass) && is_a($this->arClass, BaseActiveRecord::class, true))) {
            throw new InvalidConfigException('参数`arClass`必须继承`\yii\db\BaseActiveRecord`类');
        }

        $this->initAttributes();
    }

    /**
     * 初始化搜索模型属性
     */
    protected function initAttributes()
    {
        /** @var BaseActiveRecord $ar */
        $ar = new $this->arClass;
        $scenarios = $ar->scenarios();
        if ($this->scenario === static::SCENARIO_DEFAULT && !empty($scenarios[static::SCENARIO_SEARCH])) {
            $this->scenario = $ar->scenario = static::SCENARIO_SEARCH;
        }

        foreach ($ar->safeAttributes() as $attribute) {
            $this->defineFilterAttribute($attribute);
        }
    }

    /**
     * 定义允许自动筛选属性
     * @param string $attribute
     */
    protected function defineFilterAttribute($attribute)
    {
        $this->defineAttribute($attribute);
        $this->_allowFilterAttributes[] = $attribute;
    }

    /**
     * 返回是否存在关联
     * @return bool
     */
    public function hasRelations()
    {
        return !empty($this->_relations);
    }

    /**
     * 返回是否存在指定关联
     * @return bool
     */
    public function hasRelation($name)
    {
        return array_key_exists($name, $this->_relations);
    }

    /**
     * 返回关联
     * @return array
     */
    public function getRelations()
    {
        return $this->_relations;
    }

    /**
     * 设置关联
     * @param array $value
     */
    public function setRelations($value)
    {
        foreach ($value as $relation => $attributes) {
            $this->setInternalRelations($relation);
            $this->setInternalAttributes($relation, (array)$attributes);
        }
    }

    /**
     * 设置内部关联
     * @param string $relation
     */
    protected function setInternalRelations(string $relation)
    {
        foreach (explode('.', $relation) as $i => $part) {
            $method = 'get' . $part;

            if ($i == 0) {
                $key = $part;
                $relationClass = !$this->hasRelation($key) && method_exists($this->arClass, $method)
                    ? call_user_func([new $this->arClass, $method])
                    : null;
            } else {
                $pkey = $key;
                $key .= '.' . $part;
                $relationClass = !$this->hasRelation($key) && $this->hasRelation($pkey)
                && !empty(($className = $this->_relations[$pkey]['className']))
                && method_exists($className, $method)
                    ? call_user_func([new $className, $method])
                    : null;
            }

            if ($relationClass instanceof ActiveQuery) {
                $this->_relations[$key] = [
                    'className' => $relationClass->modelClass,
                    'tableName' => call_user_func([$relationClass->modelClass, 'tableName']),
                    'attributes' => [],
                ];
            }
        }
    }

    /**
     * 设置内部关联属性
     * @param string $relation
     * @param array $attributes
     */
    protected function setInternalAttributes(string $relation, array $attributes)
    {
        if (!$this->hasRelation($relation)) {
            return;
        }

        $relationClass = $this->_relations[$relation]['className'];
        foreach ($attributes as $attribute) {
            $attributeName = $relation . '.' . $attribute;
            if (call_user_func([new $relationClass, 'hasAttribute'], $attribute)) {
                $this->_relations[$relation]['attributes'][$attribute] = $attributeName;
                $this->defineFilterAttribute($attributeName);
            }
        }
    }

    /**
     * 返回指定属性的筛选器
     * @param string $attribute
     * @return mixed|null
     */
    public function getFilter($attribute)
    {
        return $this->_filters[$attribute] ?? null;
    }

    /**
     * 返回自动筛选条件的属性
     * @return array
     */
    public function getFilters()
    {
        return $this->_filters;
    }

    /**
     * 定义自动筛选条件的属性
     * @param string $attribute
     * @param string|callable|null $filter
     */
    public function setFilter(string $attribute, $filter = null)
    {
        !$this->hasAttribute($attribute) && $this->defineAttribute($attribute);

        if ($filter === null || in_array($filter, static::filterTypes()) || is_callable($filter)) {
            $this->_filters[$attribute] = $filter;
        }
    }

    /**
     * 设置自动筛选条件的属性
     * @param array $filters
     */
    public function setFilters($filters)
    {
        foreach ($filters as $attribute => $filter) {
            $this->setFilter($attribute, $filter);
        }
    }

    /**
     * 返回数据绑定
     * @return ActiveDataProvider
     */
    public function getDataProvider()
    {
        $dataProvider = new ActiveDataProvider([
            'query' => forward_static_call([$this->arClass, 'find']),
            'pagination' => [
                'forcePageParam' => false,
            ],
        ]);

        foreach ($this->providerOptions as $name => $value) {
            if ($dataProvider->pagination->canSetProperty($name)) {
                $dataProvider->pagination->$name = $value;
            }
            if ($dataProvider->sort->canSetProperty($name)) {
                $dataProvider->sort->$name = $value;
            }
        }

        return $dataProvider;
    }

    /**
     * 通用搜索
     * @param array $params
     * @return ActiveDataProvider
     * @throws InvalidConfigException
     */
    public function search($params = [])
    {
        $dataProvider = $this->getDataProvider();

        if (!($this->load($params) && $this->validate())) {
            // 验证失败时返回空记录
            if ($this->returnEmptyWhenValidateFails) {
                $dataProvider->query->where('0=1');
            }
            return $dataProvider;
        }

        // 创建自动筛选条件过滤器
        $this->createAutoFilter($dataProvider->query);

        // 设置允许排序属性
        foreach ($this->attributes() as $attribute) {
            if (in_array($attribute, $this->_allowFilterAttributes)) {
                $dataProvider->sort->attributes[$attribute] = [
                    'asc' => [$this->getTableField($attribute) => SORT_ASC],
                    'desc' => [$this->getTableField($attribute) => SORT_DESC],
                ];
            }
        }

        return $dataProvider;
    }

    /**
     * 创建自动筛选条件过滤器
     * @param QueryInterface $query
     */
    public function createAutoFilter(QueryInterface $query)
    {
        // 加入joinWith关联
        if ($this->hasRelations()) {
            $query->joinWith(array_keys($this->getRelations()));
        }

        foreach ($this->getAttributes() as $attribute => $value) {
            // 直接排除掉值为空的情况
            if (static::isEmpty($value)) {
                continue;
            }

            $filter = $this->getFilter($attribute);
            if (in_array($attribute, $this->_allowFilterAttributes) && !is_callable($filter)) {
                static::createFilterCondition($query, $this->getTableField($attribute), $value, $filter);
            } else if (is_callable($filter)) {
                $filter($this, $query, $attribute, $value);
            }
        }
    }

    /**
     * 创建自动筛选条件
     * @param QueryInterface $query
     * @param string $field
     * @param mixed $value
     * @param string|null $filter
     */
    public static function createFilterCondition(QueryInterface $query, string $field, $value = null, $filter = null)
    {
        if ($filter === static::FILTER_RANGE) {
            list($min, $max) = (array)$value;
            $query->andFilterWhere(['>=', $field, trim($min)]);
            $query->andFilterWhere(['<=', $field, trim($max)]);
        } elseif ($filter === static::FILTER_MATCH) {
            $query->andFilterWhere(['like', $field, $value]);
        } else {
            $query->andFilterWhere([$field => $value]);
        }
    }

    /**
     * 返回关联属性
     * @return array
     */
    public function relationAttributes(): array
    {
        $result = [];
        foreach ($this->getRelations() as $relation) {
            foreach ($relation['attributes'] as $name => $attribute) {
                $result[$attribute] = $relation['tableName'] . '.' . $name;
            }
        }
        return $result;
    }

    /**
     * 返回自动识别是否带表名的字段名
     * @param string $attribute
     * @return string
     */
    public function getTableField($attribute): string
    {
        if (!$this->hasRelations()) {
            return $attribute;
        }
        return $this->relationAttributes()[$attribute]
            ?? forward_static_call([$this->arClass, 'tableName']) . '.' . $attribute;
    }

    /**
     * 验证范围筛选属性
     * @param string $attribute
     * @param array $params
     */
    public function validateFilterRange($attribute, $params)
    {
        if ($this->hasErrors()) {
            return;
        }

        $value = $this->getAttribute($attribute);
        if (is_string($value)) {
            if (strpos($value, $this->filterRangeParam) === false) {
                $this->addError($attribute, '范围筛选必须含有`~`符号');
                return;
            }
            $value = explode($this->filterRangeParam, $value);
            $this->setAttribute($attribute, $value);
        }

        if (!is_array($value)) {
            $this->addError($attribute, '范围筛选格式有误');
            return;
        }
    }

    /**
     * @inheritDoc
     */
    public function rules()
    {
        $rules = [[$this->attributes(), 'safe']];

        $rangeFilters = [];
        foreach ($this->getFilters() as $attribute => $filter) {
            if ($filter === static::FILTER_RANGE) {
                $rangeFilters[] = $attribute;
            }
        }

        if (!empty($rangeFilters)) {
            $rules[] = [$rangeFilters, 'validateFilterRange'];
        }

        return $rules;
    }

    /**
     * @inheritDoc
     */
    public function scenarios()
    {
        return [$this->scenario => $this->attributes()];
    }

    /**
     * @inheritDoc
     */
    public function formName()
    {
        return parent::formName() . '_' . sprintf('%x', crc32($this->arClass));
    }

    /**
     * 判断是否空值
     * @param mixed $value
     * @return bool
     */
    public static function isEmpty($value)
    {
        return $value === '' || $value === [] || $value === null || is_string($value) && trim($value) === '';
    }

    /**
     * 返回所有筛选类型
     * @return string[]
     */
    public static function filterTypes()
    {
        return [static::FILTER_DEFAULT, static::FILTER_MATCH, static::FILTER_RANGE];
    }

    /**
     * 打印调试Sql语句
     * @param QueryInterface $query
     * @param bool $end 是否结束中断输出
     */
    public static function dumpSql(QueryInterface $query, bool $end = false)
    {
        var_dump($query->createCommand()->rawSql);
        $end && exit;
    }

    /**
     * 实例化通用搜索模型
     * @param string $arClass
     * @param string $scenario
     * @return SearchModel
     * @throws InvalidConfigException
     */
    public static function new(string $arClass, string $scenario = Model::SCENARIO_DEFAULT): SearchModel
    {
        return Yii::createObject([
            'class' => static::class,
            'arClass' => $arClass,
            'scenario' => $scenario
        ]);
    }
}
